Day 2 列過一張角色矩陣。今天把它變成可執行的權限設計。
| 角色 | 上傳 | 看去識別結果 | 還原 | 改政策 | 看稽核 |
|---|---|---|---|---|---|
| 一般使用者 | ✅ | ✅ | ❌ | ❌ | ❌ |
| 場景管理者 | ❌ | ✅ | ❌ | ✅ | ❌ |
| 授權還原者 | ❌ | ✅ | ✅ | ❌ | ❌ |
| 稽核人員 | ❌ | ❌ | ❌ | ❌ | ✅ |
一般使用者(承辦律師)沒有還原權限,這是最常被質疑的一點。「他自己上傳的文件,為什麼不能看原文?」
因為他本來就有原文——他從文件管理系統拿到的。他不需要「從 Token 還原」這個能力。
需要還原的是模型產出的結果:模型寫了一份摘要,裡面有 Token,律師要把它變成可用的文件。這個動作才需要還原。
而這個動作應該是可控的,因為它是唯一一個「批次產生明文個資」的操作。一份 50 頁的分析報告一次還原,等於一次產出幾百筆明文個資。
場景管理者不能還原。 他能改偵測政策、能決定哪些欄位要保護——這是很大的權力(他可以把某個 infoType 關掉)。正因為權力大,更不能同時給他還原權限。這是職務分離。
稽核人員什麼都不能做,只能看日誌。 而且他看的日誌裡不應該有明文個資——稽核需要知道「誰在什麼時候還原了 3 筆 PERSON 類型的 Token」,不需要知道還原出來是誰。
# 授權還原者
title: "PII Reidentifier"
stage: "GA"
includedPermissions:
- dlp.deidentifyTemplates.get
- cloudkms.cryptoKeyVersions.useToDecrypt
- resourcemanager.projects.get
# 場景管理者
title: "PII Policy Manager"
stage: "GA"
includedPermissions:
- dlp.inspectTemplates.create
- dlp.inspectTemplates.update
- dlp.deidentifyTemplates.create
- dlp.deidentifyTemplates.update
# 注意:沒有 cloudkms 的任何權限
# 稽核人員
title: "PII Auditor"
stage: "GA"
includedPermissions:
- logging.logEntries.list
- logging.views.access
# 沒有任何 dlp 或 kms 權限
關鍵在於 cloudkms.cryptoKeyVersions.useToDecrypt 這一項。它是還原能力的唯一來源,只有授權還原者的服務帳號應該有。
單純的角色綁定還不夠。應該加上條件,限制時間、來源、範圍:
bindings:
- role: "projects/PROJECT/roles/piiReidentifier"
members:
- "group:legal-authorized-reidentifiers@example.com"
condition:
title: "office_hours_and_internal_only"
description: "僅限上班時間、內網來源"
expression: |
request.time.getHours("Asia/Taipei") >= 9 &&
request.time.getHours("Asia/Taipei") < 18 &&
request.time.getDayOfWeek("Asia/Taipei") >= 1 &&
request.time.getDayOfWeek("Asia/Taipei") <= 5
時間限制的價值不在於阻止內部人員(他們上班時間也能做壞事),而在於縮小攻擊窗口。憑證外洩後,攻擊者只有工作日白天能用,而那正是異常行為最容易被發現的時候。
權限只是第一關。每一次還原請求都應該是一個有理由的、可歸屬的事件:
@dataclass
class ReidentifyRequest:
actor: str # 誰
scope_id: str # 哪個案件
tokens: list[str] # 哪些 Token(不是「整份文件」)
reason_code: str # 理由代碼
reason_text: str # 理由說明
approval_ref: str | None # 高風險時的核准單號
trace_id: str # 串接原始請求
reason_code 用代碼而不是自由文字,是為了讓稽核可以做統計分析:
REASON_CODES = {
"DRAFT_OUTPUT": "產出正式文書",
"CLIENT_CONTACT": "聯絡當事人",
"COURT_FILING": "法院遞狀",
"AUDIT_REVIEW": "內部查核",
"OTHER": "其他(需填說明)",
}
有了代碼,你可以問:「上個月 80% 的還原理由都是 OTHER,為什麼?」自由文字做不到這件事。
tokens: list[str] 這個設計是刻意的。還原的單位應該是 Token,不是文件。
因為整份還原意味著:一次操作產生整份文件的所有明文個資。而使用者實際需要的,通常只是其中幾個欄位(例如把摘要裡的當事人姓名填回去)。
def reidentify(req: ReidentifyRequest):
check_permission(req.actor, "pii:reidentify")
check_scope_access(req.actor, req.scope_id)
if len(req.tokens) > BULK_THRESHOLD:
require_approval(req.approval_ref, req.actor)
check_rate_limit(req.actor, len(req.tokens))
results = {}
for token in req.tokens:
results[token] = vault.resolve(token, req.scope_id)
audit_log.write({
"action": "reidentify",
"actor": req.actor,
"scope": req.scope_id,
"token_count": len(req.tokens),
"token_hashes": [sha256(t) for t in req.tokens], # 不是原值
"reason_code": req.reason_code,
"reason_text": req.reason_text,
"trace_id": req.trace_id,
"ts": now(),
})
return results
三個防護:超過門檻要核准(防批次濫用)、速率限制(防慢速大量竊取)、逐筆記錄 Token 雜湊(可事後追查是哪些欄位,但日誌裡沒有明文)。
D9 說過,遮罩和概化的欄位不可還原。還原請求碰到這些欄位時,不能報錯,要明確告知:
{
"reidentified": {
"TOKEN_PERSON_a7f3": "王小明",
"TOKEN_PHONE_b21c": "0912-345-678"
},
"not_reidentifiable": [
{"field": "birth_date", "method": "generalization",
"note": "已概化至年,無法還原至日"},
{"field": "health_info", "method": "redaction",
"note": "特種個資已遮蔽,設計上不可還原"}
]
}
這個回應格式很重要,因為它讓使用者理解系統的能力邊界,而不是以為系統壞了。而且 note 欄位等於在每一次操作中重申了設計原則。
明天把稽核軌跡做完整。
我是 Fngi,專注在 AI 資安、LLM 紅隊與 AI 治理框架落地。
IG:@aid3fend
有想討論的架構細節或不同意見,留言或私訊都歡迎。